home *** CD-ROM | disk | FTP | other *** search
- LIFE LITE PRIMER EDITION
- WRITTEN BY IAN SHARPE
- ----------------------------------------------------------------------
- THE GAME OF LIFE
-
- The Game Of Life was invented by the mathematician John Conway and
- first reached a wide public when it was written up in Scientific
- American in 1970 or thereabouts In those days it was mostly played on
- squared paper. Nowadays computers take all the hard work out of this
- fascinating invention. To some it's nothing more than a toy; to others
- it and related cellular automata are subjects for serious study.
-
- In this implementation the screen is divided into a grid of cells 40
- wide by 24 deep. A cell may be live (red) or dead (white). You begin by
- creating the first generation of live cells, or seed. Use the cursor
- keys to move the cursor, and the spacebar to toggle a cell between
- live and dead.
-
- Press [Return], and the program will quickly create successive
- generations of cells. A new generation is created by scanning every
- cell and counting up the number of live neighbours. A neighbour is
- classed as the eight cells that border horizontally, vertically and
- diagonally.
-
- The cell under scrutiny is live or dead in the next generation
- according to the following rules:
-
- * None or one live neighbour: Dies of loneliness.
- * Two live neighbours: Stays in the same state as it is now.
- * Three live neighbours: Lives in the next generation.
- * Four or more live neighbours: Dies of suffocation.
-
- When every cell on the screen has had its state assessed and made live
- or dead according to the rules, the new generation is complete and the
- process repeats.
-
- Here are some interesting patterns that I found lying in a gutter on
- the information superhighway. A # represents a live cell, a dot is a
- blank. Where components of a shape are separated by blanks, the exact
- number of blanks is crucial - for example the eight between the
- rightmost block and the rest of the shape in Glider Gun. You need to
- view these in a monospaced font, such as the screen font or Courier.
-
- ..#.
- ...#
- ### ####
- BLINKER GLIDER
-
-
- ##...##
- #.#.#.#
- #.#.#.# #..#.
- ..#.#.. ....#
- ..#.#.. #...#
- .##.##. .####
- .##.##.
- PULSATOR TRAIN
-
-
-
- ....##..
- ..#...#.
- ## .#.....# ##
- ##..##.#...#........##
- .#.....#
- ..#...#.
- ....##..
- SHUTTLE
-
-
-
- ##
- ....##....#...
- .## .#####.##..
- ##.....### #..##....#.
- ## .## ...##........#........##
- ....## ..#.......# ##
- ## ...#
- ..#.
- ##..
- GLIDER GUN - it just about fits. Keep it well clear of the edges, and
- put it slightly towards the lower half of the screen.
-
-
- ----------------------------------------------------------------------
- HOW THE PROGRAM WORKS
-
- I suggest you print out LIFELITE.CPP (it's about three pages long) so
- that you can easily read the comments and see the code that's being
- referred to.
-
- The state of play is stored in the 3D array world[][][]. Conceptually
- you can think of it as two pages of graph paper. At any one time, one
- page will hold the existing generation while the new generation is
- written to the other. The new generation then becomes the existing
- generation, the next generation is written back to the original page.
- It's global because several functions need to access it. The array
- subscripts are world[row][column][page].
-
- A page in the array is larger than the screen area so that it has a
- border of unused cells around the playing area. This is so that the
- bit of the program that adds up the number of live cells around the
- cell currently being examined doesn't have to take special action when
- considering cells at the edge of the screen. Not having to make
- special arrangements here makes the programming simpler.
-
- The #define constants are purely to make the program code more
- readable and easier to alter.
-
- The code itself is broken into six functions:
-
-
- main()
- ------
- Start here, as it gives you an overview of the program's architecture
- and flow of control.
-
- Main() spends most of its time in a loop which constantly cycles
- between two functions: input_seed() in which the user inputs a new
- seed, and breed() which successively breeds and displays new
- generations until the user presses one of the two keys that allows him
- or her to exit this function. Both input_seed() and breed() return a
- value which reports what the user wants to do. This return value will
- either trigger the ending of the loop (and therefore the program) or a
- new loop cycle.
-
- At the end of main(), immediately before the program terminates, it
- calls tidyup() to restore certain aspects of the display that were
- changed during program execution. Otherwise the screen would be messy
- upon return to DOS.
-
-
- input_seed()
- ------------
- Enables the user to input a new starting generation. It first
- clears all array elements to DEAD, makes the cursor invisible, sets
- white as the background colour, then wipes the screen.
-
- Next it prints the message at the bottom the screen. It sets the
- background colour to green, moves the cursor to the bottom left, and
- clears to the end of the lion, producing a green strip. Then it prints
- the text.
-
- All these text/background colour functions etc and the #define values
- used as parameters are detailed in the online help.
-
- After setting the colours to the ones used for printing live (red) and
- dead (white) cells we initialise variables x and y. These are used to
- keep track of the input cursor, and the initialisation values put it
- near the centre of the screen when it first appears. The function
- movecursor() moves the cursor from the first set of coordinate
- parameters to the second. Using the same parameters twice allows us to
- place the cursor on the screen for the first time.
-
- Note that the cursor we're talking about here is not the normal text
- screen hardware cursor. Thus was made invisible earlier, though it can
- still be moved around the screen in order to set the position at which
- the next character will be printed. However, we don't want to use it
- for cell input because a cell is two characters wide whereas the
- hardware cursor is only one character wide. It would look odd.
- Instead, the input cursor is artificially created by using a pair of ≡
- characters. These are moved around by the program in response to
- presses of the cursor keys, and the colour of the cursor characters
- and their background are changed according to the state of the cell
- they're sitting on.
-
- Next comes a loop in which cursor and other keypresses are processed.
- This loop cannot end because the condition, being a literal non-zero
- number, is forever true. Exit from this function is only via the two
- return() statements triggered by the [Return] and [Q] keypresses.
-
- The loop executes only one action: a SWITCH statement within which the
- main business of input_seed() is conducted. Getch() waits for the user
- to press a key, which is converted to upper case by toupper(), the
- return value from which causes the SWITCH to run down its list of CASE
- statements looking for a match:
-
- The '\r' character is the [Return] key. It causes the function to
- return, passing back a code which main() interprets as the signal to
- keep looping. Main()'s next action will be to execute the breed()
- function.
-
- 'Q' is the quit key. This causes input_seed() to return a code which
- main() interprets as the signal to stop its main loop, and thereby end
- the program.
-
- I use #defines for QUIT and KEEP_GOING, rather than quoting the
- literal codes, simply to make the program more readable. You can read
- and understand parts of the program that use these codes without
- having to find out what some raw numbers mean.
-
- A space - ' ' - toggles the state of the cell under the cursor. We
- always input a seed on page [0] of the world array. Since x,y on the
- screen is column, row and the first pair of subscripts to world[] are
- row, column, x and y are switched round. ^ is a logical operator
- meaning XOR - eXclusive OR. A useful property of XOR is that any
- number XOR itself is zero, and any number XOR zero is itself. So:
-
- 1 XOR 1 = 0 and 0 XOR 1 = 1
-
- And since DEAD=0 and LIVE=1,
-
- DEAD XOR LIVE=LIVE and LIVE XOR LIVE=DEAD
-
- XORing provides an easy way of switching a variable between two values
- without having to test its present value. ^= follows the C++
- convention of shortening: x = x ^ 1 to: x ^= 1.
-
- After changing the state of the current cell in the world[] array, we
- call up the function showcell which displays the cell at the
- coordinates specified. This will wipe out the cursor, so we then call
- movecursor() again to re-display it.
-
- We don't want to fall through the floor into the next CASE, so we
- break out of the SWITCH block. This will take program execution out
- into the loop, so the next things to be executed are the WHILE( 1 )
- then the SWITCH again.
-
- Case '\0' is triggered when the keypress yields a value of zero. This
- happens with certain keys, such as the cursor keys. Instead of
- generating a single character code, they produce two codes. The first
- one is always zero. After getch() has taken the '\0' from the keyboard
- buffer, the other code will be left sitting in there, waiting for the
- next keyboard read to pluck it out. It isn't certain that the next
- keyboard character will signify a cursor movement - other keys use
- this zero-plus-code code. However, for the moment we assume that we're
- going to have to move the cursor after then next getch() call. In
- preparation for this, we put the present position of the cursor in the
- variables nx and ny. This is because moving the cursor follows this
- logic:
- * Overprint the old cursor position with the current state of
- the cell, thus wiping the cursor characters.
- * Print the cursor characters at the new screen position,
- adjusting the foreground and background colours according to
- the state of the new cell.
- This requires us to have two sets of coordinates - old cursor position
- and new cursor position, so we have the position in two sets of
- variables.
-
- We then use getch() to get the waiting character from the keyboard
- buffer, and use its value as the basis of another SWITCH block. It is
- perfectly legal to have nested SWITCHES like this, though this
- function is getting rather long and for the sake of legibility could
- be broken into two shorter functions.
-
- The output from this second call to getch() isn't converted to upper
- case because the codes generated by the cursor keys are them same
- whether or not [Shift] or [Caps Lock] is pressed.
-
- The CASE statements use #define values for legibility again. You can
- see the underlying character values at the head of the program. Each
- CASE adjusts a cursor ordinate. To avoid repetitious typing, we only
- test that the new value is legal (ie it hasn't stepped off the screen)
- when all the possible program flow routes through this section of code
- have converged below this inner SWITCH block.
-
- We could have used a lot of IFing and ELSEing to test the cursor
- position but the (test) ? <true result> : <false result>; operator is
- far neater. If the new cursor ordinate is still on the screen, its
- present value is overwritten with its present value - no change, in
- other words. If the cursor would be off-screen, the value in nx or ny
- is replaced with the old value, pulling it back to where it was.
-
- The program doesn't now need to worry about whether or not the cursor
- actually moves to a new cell. It just calls up movecursor() with the
- old and new values of the cursor coordinates, and it doesn't matter if
- all we're doing is re-printing the cursor on top of itself. Similarly,
- when we update the variables x and y with the present position of the
- cursor, it doesn't matter if there was no movement. Occasionally doing
- a bit of unnecessary work in a situation where execution speed isn't
- an issue makes the program simpler to understand, and actually causes
- it to execute fewer instructions in the more common situation of
- having the cursor away from the edge.
-
- The closing brace following the update of the variables is the bottom
- of the outer SWITCH, so we're taken back to the top of the loop to
- wait for a new keypress.
-
-
- breed()
- -------
- This function successively breeds and displays new generations until
- the user presses [Return] or [Q].
-
- At the start of the function we use a procedure similar to the one at
- the head of input_seed() to print a help bar at the bottom of the
- screen. We don't clear the screen or the array, though, because the
- newly-input first generation is breed()'s starting position.
-
- Remember that the world[] array is conceptually two pages, and that
- the program reads one page (the old generation) while writing to the
- other (the new generation). It then swaps round so that the new
- generation becomes the old one, and the previous old generation is
- overwritten with its grandchildren.
-
- The constant flipping of perception of which pages constitute old and
- new generations is achieved by using variables oldgen and newgen as the
- third subscript to the world[] array. In one cycle oldgen=0 and
- newgen=1. In the next cycle oldgen=1 and newgen=0. Before starting the
- loop which controls the breeding cycle we initialise these values. We
- also initialise a variable named healthy. In the body of the loop the
- value of healthy acts as a monitor on the state of health of the
- latest generation. A zero value indicates that there are no live
- cells. This triggers the printing of a message. It's more than a bit
- of fun - it keeps inattentive users feeling comfortable because they
- know the program hasn't crashed and that the programmer does pay
- attention to detail. It's a good approach to adopt, but keep it subtle
- and effective. Otherwise your programs will feel fussy and over-
- elaborate (and therefore stressful to use). They will also contain
- extra bugs because you spent so much time making sure everybody's kept
- fully aware of how clever you are, rather than concentrating on core
- functionality (trebly stressful, and a double own-goal).
-
- Breed()'s main loop is another never-ending cycle that only terminates
- when you force the function to return following an appropriate
- keypress.
-
- The breeding is done by a nested loop which continues until you press
- a key. If you look below this loop you will see a SWITCH block which
- reads and processes the keypress. If the pressed key wasn't one of two
- listed in the CASE statements, program execution loops back up to the
- WHILE( 1 ) causing the breeding cycle to resume. Notice that in
- contrast to the main loop in input_seed(), we need braces here. The
- inner WHILE and the SWITCH are two actions for the main loop to
- execute.
-
- Looking at the inner WHILE now, the first thing it does is check the
- value of the variable healthy. Volatile local variables start life
- with a random value - they inherit whatever garbage was sitting in the
- bytes that are newly allocated to them. This is why we earlier
- initialised healthy with a non-zero value - had we not done so, and had
- healthy accidentally already contained zero, the actions after this IF
- test would have been executed, even if it was inappropriate for the
- message to be printed. On subsequent loop cycles, if healthy does
- contain zero, the message is printed. To avoid it flickering as it is
- repeatedly overwritten with dead cells, the CONTINUE statement forces
- execution to jump to the head of the loop. This skips all the stuff
- that follows, curtailing operations to a keytest/print message cycle.
-
- Healthy is nothing more than a running total of the sums of live
- neighbours calculated for each cell. It therefore has to be set to
- zero before we start calculating. A completely dead playing field will
- leave it at that value.
-
- The uses a pair of nested loops to scan every cell in turn. It only
- makes calculations for visible cells - the ones in the off-screen
- border (remember that we made world[] bigger than the screen) are not
- included in the scan. However, when calculating the sum of live
- neighbours for cells at the edge of the screen, the cells in the border
- are part of the sum. This makes the programming easy, but means that
- the edges of the world artificially stunt growth. A better approach
- would be to have the world wrap round, so that top and bottom edges
- are joined, as are left and right edges. Cells that move off one edge
- then appear on the opposite edge. This wouldn't be hard to arrange -
- don't delay, enrol today as a valued founder subscriber to the Life Pro
- upgrade savings plan!
-
- The variable sum is just a total of the values in neighbouring cells.
- It is permissible to split complicated lines in this way because the ;
- marks the end of the expression, not a carriage return.
-
- Having summed up the neighbours, we use a SWITCH block to decide the
- state of the cell in the next generation. This is a straight
- application of the rules of the game, following which we call
- showcell() to display the state of the new cell. Healthy is updated.
-
- When the loop pair has run its course, the XOR technique described
- earlier is used to flip the values of oldgen and newgen.
-
- Calculating and displaying in the same loop gives maximum overall
- program execution speed. Speed could be sacrificed to give a cleaner
- display update. All the calculations would be done in one pair of
- nested loops, and the update in a separate pair. The screen update
- part of cycle would be quicker, because it doesn't have to wait for a
- cell to be calculated before it is printed.
-
- This might be worthwhile - the program runs too quickly on a fast PC
- anyway, so on most PCs we could afford to lose some speed. An
- associated upgrade would be to introduce a mechanism to automatically
- limit the speed on faster PCs, so that on machines faster than, say, a
- 25MHz 486, it didn't go any quicker. Life Pro Gold?
-
- Program execution has reached the bottom of the inner WHILE loop, and
- jumps back to the top. If the user pressed a key recently, it is analysed
- as mentioned earlier.
-
-
- showcell()
- ----------
- Displays the current state of a cell on the screen. It converts
- world[] subscript coordinates to screen coordinates, moves the
- hardware cursor to that place, and prints either a pair of block
- characters to make a filled square, or a pair of blanks, depending on
- the state of the cell. The main point about the translation of
- coordinates is that each cell in word[] occupies two horizontally
- adjacent text cells on the screen, so we have to double the X
- ordinate.
-
-
- movecursor()
- ------------
- Moves the artificial input_seed() cursor from one place on the screen
- to another. A pair of ≡ characters is used to mark the cursor
- position, and the colours of background and foreground are chosen to
- reflect the state of the cell the cursor is sitting on.
-
-
- tidyup()
- --------
- Called up immediately before program termination to tidy up the
- display. To avoid a nastily coloured DOS prompt messily overwriting
- the latest generation, the colours are changed to sensible values, the
- hardware cursor restored to visibility, and the screen cleared. Ideally,
- during initialisation the program would have saved the shape of the
- cursor and the current text and background colours, and restored those.
-
-
- .oOo.
-
-